﻿using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Web;
using Microsoft.Win32;
using SecurityModule.Workflows;

namespace SecurityModule
{
    /// <summary>
    /// An HTTP module meant to intercept requests to the sales tax app to:
    /// - Handle security-related functions by calling Security Service, which provides credentials management, authentication, authorization and data encryption.
    /// - Isolate the sensitve data from the app by:
    ///     - rewriting passwords
    ///     - removing Yubikey OTPs
    ///     - encrypting tax numbers when getting into the app
    ///     - decrypting tax numbers when getting out of the app
    ///     - clear any client-side code (JS) that may leak sensitive data to the app
    /// - Validate Yubikey OTPs with Yubico's validation service
    /// The code for this module is publically shared here: https://secmod4salestax.codeplex.com/
    /// </summary>
    public class SecurityModule : IHttpModule
    {
        /// <summary>
        /// A list of http request/response rewriting workflows.
        /// </summary>
        private List<IWorkflow> _workflows;

        /// <summary>
        /// An array of acceptable modules to be used by the app, adding a module with a name not in this list or doesn't start by one listed in <see cref="WhitelsitedDynamicModulePrefixes"/> will cause the app to halt.
        /// </summary>
        private static readonly string[] WhitelsitedModules = new[]
            {
                "OutputCache",
                "Session",
                "WindowsAuthentication",
                "FormsAuthentication",
                "DefaultAuthentication",
                "RoleManager",
                "UrlAuthorization",
                "FileAuthorization",
                "AnonymousIdentification",
                "Profile",
                "UrlMappingsModule",
                "ServiceModel-4.0",
                "UrlRoutingModule-4.0",
                "ScriptModule-4.0",
                "SecurityModule",
                "RaygunErrorModule"     // Used by the app for error reporting, see: https://raygun.com/
            };

        /// <summary>
        /// An array of acceptable dynamic module prefixes to be used by the app, adding a module with a name not in <see cref="WhitelsitedModules"/> or doesn't start by one of the items listed here will cause the app to halt.
        /// </summary>
        private static readonly string[] WhitelsitedDynamicModulePrefixes = new[]
            {
                "__DynamicModule_Microsoft.VisualStudio.Web.PageInspector.Runtime.Tracing.PageInspectorHttpModule, Microsoft.VisualStudio.Web.PageInspector.Runtime",
                "__DynamicModule_System.Web.WebPages.WebPageHttpModule, System.Web.WebPages",
                "__DynamicModule_System.Web.Optimization.BundleModule, System.Web.Optimization"
            };

        /// <summary>
        /// To check if <paramref name="application"/> uses any module that isn't whitelisted.
        /// </summary>
        /// <param name="application"></param>
        /// <exception cref="InvalidOperationException">Thrown when a suspecious module is found.</exception>
        private static void CheckModules(HttpApplication application)
        {
            // Check if there is any module that isn't whitelisted
            var modules = application.Modules;
            var suspeciousModules = modules.AllKeys.Where(m => !WhitelsitedModules.Contains(m) && !WhitelsitedDynamicModulePrefixes.Any(m.StartsWith)).ToArray();
            if (suspeciousModules.Length > 0)
            {
                // Halt the app with error message
                EventLog.WriteEntry("SecModule", string.Format("Suspecious module(s) '{0}' found", string.Join(",", suspeciousModules)), EventLogEntryType.Error);
                throw new InvalidOperationException(string.Format("Suspecious module(s) '{0}' found", string.Join(",", suspeciousModules)));
            }
        }

        private static void ValidateSecurityService()
        {
            string path;
            using (var key = Registry.LocalMachine.OpenSubKey("SYSTEM\\CurrentControlSet\\Services\\SecurityService"))
            {
                if (key != null)
                {
                    path = key.GetValue("ImagePath") as string;
                    if (path != null)
                        path = path.Trim('"');
                }
                else
                {
                    EventLog.WriteEntry("SecModule", "Can't start the application because SecurityService is not installed", EventLogEntryType.Error);
                    throw new InvalidOperationException("Can't start the application because SecurityService is not installed");
                }
            }

            byte[] hash;
            using (var sha1 = SHA1.Create())
            {
                using (var stream = File.OpenRead(path))
                {
                    hash = sha1.ComputeHash(stream);
                }
            }
            if (!hash.ByteArrayToString().Equals("B05386C3AEA7544C5E58F0AB7D94BAC6B996F667"))
            {
                // Halt the app with error message
                EventLog.WriteEntry("SecModule", "Can't start the application because installed SecurityService is not valid", EventLogEntryType.Error);
                throw new InvalidOperationException("Can't start the application because installed SecurityService is not valid");
            }
        }

        public void Init(HttpApplication application)
        {
            EventLog.WriteEntry("SecModule", "Initializating", EventLogEntryType.Information);
            CheckModules(application);
#if !DEBUG
            // Make sure the running Security Service is valid
            ValidateSecurityService();
            // Ensure this is the last version submitted to Codeplex, [latestChangeSetId] is a placeholder that is replaced in the deployment process prior to signing the module, please refer to https://secmod4salestax.codeplex.com/wikipage?title=Deployment%20Process.
            var latestChangeSetId = TfsVersionChecker.GetLatestChangesetId();
            if (!latestChangeSetId.Equals("[latestChangeSetId]"))
            {
                // Halt the app
                throw new InvalidOperationException("Security Module must be updated to the last version " + latestChangeSetId + ", this build version is [latestChangeSetId]");
                EventLog.WriteEntry("SecModule", "Security Module must be updated to the last version " + latestChangeSetId + ", this build version is [latestChangeSetId]", EventLogEntryType.Error);
            }
#endif

            //Subscribe to the beginrequest event
            application.BeginRequest += Application_BeginRequest;
            application.EndRequest += Application_EndRequest;

            _workflows = new List<IWorkflow>
                {
                // JS Filter
                new WFJSFilter(),

                //Login
                new WFGetHomeLogin(),                   // Tested successfully
                new WFPostHomeLogin(),                  // Tested successfully

                //Logout
                new WFPostHomeLogout(),                 // Tested successfully

                //Setup
                new WFPostSetup(),                      // Tested successfully
                new WFGetSetupVerify(),                 // Tested successfully
                new WFPostSetupVerify(),                // Tested successfully

                //Invite User
                new WFPostAppAccountInviteUser(),       // Tested successfully
                new WFGetHomeCreateUser(),              // Tested successfully
                new WFPostHomeCreateUser(),             // Tested successfully

                //Approve a new user
                new WFPostHomeNewAccountAction(),       // Tested successfully

                //Change password
                new WFPostAppAccountChangePassword(),   // Tested successfully

                //Reset password
                new WFPostHomeForgotPassword(),         // Tested successfully
                new WFGetHomeResetPassword(),           // Tested successfully
                new WFPostHomeResetPassword(),          // Tested successfully

                //Approve reset password
                new WFPostHomePasswordResetAction(),    // Tested successfully

                //Reset Yubikey
                new WFPostHomeForgotYubikey(),          // Tested successfully
                new WFGetHomeResetYubikey(),            // Tested successfully
                new WFPostHomeResetYubikey(),           // Tested successfully

                //Approve reset Yubikey
                new WFPostHomeYubikeyResetAction(),     // Tested successfully

                //Delete User
                new WFPostAppAccountDeleteUser(),       // Tested successfully

                //Data Uploading
                new WFPostAppDataUploading(),           // Tested successfully

                //Analysis
                new WFPostAppAnalysisReport(),          // Tested successfully

                //About
                new WFGetStaticPagesAbout(),

                //new WFPostAppRemittanceStoreCalculatedMissingAlerts(),

                //new WFGetAppRemittanceMissing()
            };
        }

        private void Application_BeginRequest(Object source, EventArgs e)
        {
            // Obtain HttpApplication and HttpContext objects
            var application = (HttpApplication)source;
            var context = application.Context;

            CheckModules(application);

            //DO NOT MUTATE REQUEST, IT WILL PREVENT REQUEST FILTERING

            //Execute all applicable workflows
            _workflows.ForEach(w => w.PreProcess(context));
        }


        private void Application_EndRequest(Object source, EventArgs e)
        {
            var application = (HttpApplication)source;
            var context = application.Context;

            //Execute all applicable workflows
            _workflows.ForEach(w => w.PostProcess(context));
        }

        public void Dispose()
        {
        }
    }
}
